using System;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace csammisrun.MsnLib.Utility
{
  public enum ServerConnectionType
  {
    NameServer,
    SwitchBoard
  }

  public delegate void ServerConnectionEventHandler(ServerConnection conn);
  public delegate void ServerConnectionErrorHandler(ServerConnection conn, string error);

  public class ServerConnection
  {
    #region Static server connection incrementation
    private static int _nextconnectionid = 0;
    /// <summary>
    /// Gets the next connection ID
    /// </summary>
    public static int NextConnectionID
    {
      get { return ServerConnection._nextconnectionid++; }
    }
    #endregion

    #region Static socket connection routines
    /// <summary>
    /// Encapsulates socket connection information
    /// </summary>
    class SocketConnectionData
    {
      public string Server;
      public int Port;
      public Socket Socket;
      public Delegate Callback;
    }

    /// <summary>
    /// Begins the socket creation process
    /// </summary>
    public static void CreateDirectConnectSocket(string host, int port, Delegate callback)
    {
      // Make sure the callback is the right type
      ParameterInfo[] param = callback.Method.GetParameters();
      if (param.Length != 2 || param[0].ParameterType != typeof(Socket) || param[1].ParameterType != typeof(string))
      {
        throw new ArgumentException("Callback delegate must take a Socket and a string as its parameters");
      }
      
      SocketConnectionData scd = new SocketConnectionData();
      scd.Callback = callback;
      scd.Port = port;
      scd.Server = host;
      scd.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

      try
      {
        Dns.BeginGetHostEntry(scd.Server, new AsyncCallback(CreateDCSEndDnsLookup), scd);
      }
      catch (SocketException sockex)
      {
        string message = "Cannot connect to DNS service:" + Environment.NewLine +
          sockex.Message;
        callback.DynamicInvoke(scd.Socket, message);
      }
    }
    /// <summary>
    /// Ends the DNS lookup phase of connection and begins connecting the socket to the host
    /// </summary>
    protected static void CreateDCSEndDnsLookup(IAsyncResult res)
    {
      SocketConnectionData scd = null;

      IPHostEntry hosts = null;
      try
      {
        scd = res.AsyncState as SocketConnectionData;
        hosts = Dns.EndGetHostEntry(res);
      }
      catch (Exception sockex)
      {
        string message = "Cannot resolve server:" + Environment.NewLine +
          sockex.Message;
        hosts = new IPHostEntry();
        hosts.AddressList = Dns.GetHostAddresses(scd.Server);
      }

      IPAddress address = hosts.AddressList[0];
      IPEndPoint ipep = new IPEndPoint(address, scd.Port);

      try
      {
        scd.Socket.BeginConnect(ipep, new AsyncCallback(CreateDCSEndInitialConnection), scd);
      }
      catch (SocketException sockex)
      {
        string message = "Cannot connect to server:" + Environment.NewLine +
          sockex.Message;
        scd.Callback.DynamicInvoke(scd.Socket, message);
      }
    }

    /// <summary>
    /// Ends the connection phase and returns a connected socket
    /// </summary>
    protected static void CreateDCSEndInitialConnection(IAsyncResult res)
    {
      SocketConnectionData scd = null;
      try
      {
        scd = res.AsyncState as SocketConnectionData;
        scd.Socket.EndConnect(res);
        scd.Callback.DynamicInvoke(scd.Socket, "");
      }
      catch (Exception sockex)
      {
        string message = "Can't connect to server: " + Environment.NewLine +
          sockex.Message;
        scd.Callback.DynamicInvoke(scd.Socket, message);
      }
    }
    #endregion

    private const int BUFFERLENGTH = 1500;

    private Session _parent = null;
    private ServerConnectionType _type = ServerConnectionType.NameServer;
    private CommandProcessor _proc = null;

    private bool _isconnected = false;
    private bool _isprocessing = false;
    private bool _isdead = false;
    private bool _isdisconnecting = false;

    private string _host = "";
    private int _port = 0;
    private Socket _sock = null;
    private int _id = -1;

    private byte[] _receivebuffer = new byte[BUFFERLENGTH];
    private List<byte> _fullbuffer = new List<byte>();
    private byte[] _payload = null;
    private byte[] _crlf = new byte[] { (byte)'\r', (byte)'\n' };

    private AsyncCallback _sendcallback;
    private AsyncCallback _recvcallback;

    public event ServerConnectionEventHandler Connected;
    public event ServerConnectionErrorHandler ConnectionLost;
    public event ServerConnectionEventHandler Disconnected;
    public event ServerConnectionEventHandler Destroyed;

    public ServerConnection(Session parent, ServerConnectionType type)
    {
      _parent = parent;
      _type = type;
      _proc = new CommandProcessor(parent);
      _proc.Connection = this;
      _id = ServerConnection.NextConnectionID;

      _sendcallback = new AsyncCallback(EndWritePacket);
      _recvcallback = new AsyncCallback(EndReceive);
    }

    #region Connect to server
    /// <summary>
    /// Connects to the specified host and port
    /// </summary>
    public bool Connect(string host, int port)
    {
      if (IsConnected)
      {
        Disconnect();
      }

      _host = host;
      _port = port;
      _isdisconnecting = false;

      // Reset buffers
      _receivebuffer = new byte[BUFFERLENGTH];
      _fullbuffer = new List<byte>();

      if (_parent.UseHTTP)
      {
        // Fire up the HTTP connection
        return true;
      }

      _parent.ProxiedSocketFactory(_host, _port, new ProxiedSocketFactoryResultHandler(EndInitialConnection));
      return true;
    }

    private void EndInitialConnection(Socket socket, string errormsg)
    {
      if (!String.IsNullOrEmpty(errormsg))
      {
        Disconnect(errormsg);
        return;
      }

      _sock = socket;
      _isprocessing = true;
      _isconnected = true;
      // Start reading from the socket
      try
      {
        _sock.BeginReceive(_receivebuffer, 0, _receivebuffer.Length, SocketFlags.None, _recvcallback, null);
      }
      catch (SocketException sockex)
      {
        string message = "Cannot begin receiving data:" + Environment.NewLine +
            sockex.Message;
        Disconnect(message);
      }

      if (this.Connected != null)
      {
        this.Connected(this);
      }
    }
    #endregion

    #region Disconnect from server
    public void Disconnect()
    {
      Disconnect("");
    }

    private void Disconnect(string error)
    {
      if (_isdisconnecting == true)
      {
        return;  // Don't...don't report it twice.  Come on.
      }

      _isdisconnecting = true;
      _isconnected = false;

      // HTTP is stateless, so just fake the disconnect if HTTP is being used
      if (!_parent.UseHTTP)
      {
        _sock.Close();
      }

      Logging.WriteString(error);

      if (String.IsNullOrEmpty(error))
      {
        if (this.Disconnected != null)
        {
          this.Disconnected(this);
        }
      }
      else
      {
        if (this.ConnectionLost != null)
        {
          this.ConnectionLost(this, error);
        }
      }
    }
    #endregion

    #region Send data to server
    private struct SendData
    {
      public byte[] Buffer;
      public int BytesSent;
    }

    /// <summary>
    /// Asynchronously sends a packet to the server
    /// </summary>
    public bool SendPacket(byte[] data)
    {
      if (!IsConnected)
      {
        return false;
      }

      if (!_parent.UseHTTP)
      {
        SendData sd = new SendData();
        sd.Buffer = data;
        sd.BytesSent = 0;

        try
        {
          _sock.BeginSend(sd.Buffer, sd.BytesSent, sd.Buffer.Length, SocketFlags.None, _sendcallback, sd);
        }
        catch (SocketException sockex)
        {
          string message = "Cannot send data:" + Environment.NewLine +
            sockex.Message;
          Disconnect(message);
          return false;
        }
      }
      else
      {
        // Return value from HTTP connection send
      }

      return true;
    }

    private void EndWritePacket(IAsyncResult res)
    {
      try
      {
        SendData sd = (SendData)res.AsyncState;
        sd.BytesSent += _sock.EndSend(res);
        if (sd.BytesSent < sd.Buffer.Length)
        {
          _sock.BeginSend(sd.Buffer, sd.BytesSent,
            sd.Buffer.Length - sd.BytesSent,
            SocketFlags.None, _sendcallback, sd);
        }
      }
      catch (Exception ex)
      {
        string message = "Cannot send data:" + Environment.NewLine +
            ex.Message;
        Disconnect(message);
      }
    }
    #endregion

    #region Read data from server
    /// <summary>
    /// Set the length of the payload that the connection should be reading from the server
    /// </summary>
    public void SetExpectedPayloadLength(int length)
    {
      if (ReadingInPayload)
      {
        throw new Exception("Payload length cannot be set while an existing payload is being read");
      }

      _payload = new byte[length];
    }

    private void EndReceive(IAsyncResult res)
    {
      int bytesread = 0;
      int payloadlength = 0;
      int remainingbuffer = 0;
      int cmdindex = -1;
      string cmdstr = "", cmdname = "";
      string[] cmdparams = null;
      byte[] bufferarray = null;

      try
      {
        bytesread = _sock.EndReceive(res);
      }
      catch (Exception ex)
      {
        string message = "Cannot successfully end receipt of data:" + Environment.NewLine +
            ex.Message;
        Disconnect(message);
        return;
      }

      if (bytesread == 0)
      {
        ResetReceiver();
        return;
      }

      // Pre-resize the buffer so appending the data doesn't trigger a lot of automatic resizing
      if (_fullbuffer.Capacity < _fullbuffer.Count + bytesread)
      {
        _fullbuffer.Capacity = _fullbuffer.Count + bytesread;
      }

      // Append the received data
      for (int i = 0; i < bytesread; i++)
      {
        _fullbuffer.Add(_receivebuffer[i]);
      }

      // Process commands / payloads out of the buffer
      while (_fullbuffer.Count > 0 && IsConnected && !IsDead)
      {
        bufferarray = _fullbuffer.ToArray();
        // Find the end of the first command
        cmdindex = Marshal.IndexOf(bufferarray, _crlf, 0);
        if (cmdindex == -1)
        {
          // If a full command hasn't been read in, read in more data
          break;
        }

        // Get the buffer contents up to the CRLF as a UTF-8 string
        cmdstr = Encoding.UTF8.GetString(bufferarray, 0, cmdindex);
        cmdname = cmdstr.Substring(0, 3).ToUpper();
        if (cmdname == "XFR")
        {
          cmdname = cmdstr.Substring(0, 3).ToUpper();
        }

        // Check to see if the command is a type that requires a payload
        if (cmdname == "MSG" || cmdname == "NOT" || cmdname == "PAG" ||
            cmdname == "IPG" || cmdname == "UBX" || cmdname == "GCF")
        {
          // Get the payload length from the last parameter of the command
          cmdparams = cmdstr.Split(' ');
          payloadlength = Int32.Parse(cmdparams[cmdparams.Length - 1]);
          // Check to see if the entire payload is contained in the buffer
          remainingbuffer = _fullbuffer.Count - (cmdindex + 2);
          if (remainingbuffer >= payloadlength)
          {
            // The entire payload is available, extract it and dispatch the command
            byte[] payload = new byte[payloadlength];
            _fullbuffer.CopyTo(cmdindex + 2, payload, 0, payloadlength);
            _proc.DispatchCommand(new MsnCommand(cmdstr, payload));
          }
          else
          {
            // If the entire payload isn't here, read in more data
            break;
          }
        }
        else
        {
          // No payload required, dispatch the command
          payloadlength = 0;
          _proc.DispatchCommand(new MsnCommand(cmdstr));
        }

        if (this.IsConnected && !this.IsDead)
        {
          // Shrink the buffer by the amount of data read, if the command hasn't told the connection to shut down
          _fullbuffer.RemoveRange(0, cmdindex + 2 + payloadlength);
        }
        else
        {
          return;
        }

      } // End while(_fullbuffer.Count > 0)

      ResetReceiver();
    }

    /// <summary>
    /// Reset the receiving socket to read in more data
    /// </summary>
    private void ResetReceiver()
    {
      try
      {
        _sock.BeginReceive(_receivebuffer, 0, _receivebuffer.Length, SocketFlags.None, _recvcallback, null);
      }
      catch (ObjectDisposedException)
      {
        // Nothing
      }
      catch (SocketException sockex)
      {
        string message = "Cannot begin receiving data:" + Environment.NewLine +
            sockex.Message;
        Disconnect(message);
      }
    }
    #endregion

    /// <summary>
    /// Gets a value indicating whether or not this ServerConnection is connected
    /// </summary>
    public bool IsConnected
    {
      get { return _isconnected; }
    }

    /// <summary>
    /// Gets a value indicating whether or not this ServerConnection is in use
    /// </summary>
    public bool IsProcessing
    {
      get { return _isprocessing; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether or not this ServerConnection should be disposed
    /// </summary>
    public bool IsDead
    {
      get { return _isdead; }
      set { _isdead = value; }
    }

    /// <summary>
    /// Gets the ID of this ServerConnection
    /// </summary>
    public int ID
    {
      get { return _id; }
    }

    /// <summary>
    /// Gets the <see cref="CommandProcessor"/> being used by this connection
    /// </summary>
    public CommandProcessor CommandProc
    {
      get { return _proc; }
    }

    /// <summary>
    /// Gets a value indicating whether or not the server connection is processing a payload or a command
    /// </summary>
    private bool ReadingInPayload
    {
      get { return (_payload != null && _payload.Length > 0); }
    }
  }
}
